﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace winPEAS.TaskScheduler
{
    internal static class XmlSerializationHelper
    {
        public static object GetDefaultValue([NotNull] PropertyInfo prop)
        {
            var attributes = prop.GetCustomAttributes(typeof(DefaultValueAttribute), true);
            if (attributes.Length > 0)
            {
                var defaultAttr = (DefaultValueAttribute)attributes[0];
                return defaultAttr.Value;
            }

            // Attribute not found, fall back to default value for the type 
            if (prop.PropertyType.IsValueType)
                return Activator.CreateInstance(prop.PropertyType);
            return null;
        }

        private static bool GetPropertyValue(object obj, [NotNull] string property, ref object outVal)
        {
            PropertyInfo pi = obj?.GetType().GetProperty(property);
            if (pi != null)
            {
                outVal = pi.GetValue(obj, null);
                return true;
            }
            return false;
        }

        private static bool GetAttributeValue(Type objType, Type attrType, string property, bool inherit, ref object outVal)
        {
            object[] attrs = objType.GetCustomAttributes(attrType, inherit);
            if (attrs.Length > 0)
                return GetPropertyValue(attrs[0], property, ref outVal);
            return false;
        }

        private static bool GetAttributeValue([NotNull] PropertyInfo propInfo, Type attrType, string property, bool inherit, ref object outVal)
        {
            Attribute attr = Attribute.GetCustomAttribute(propInfo, attrType, inherit);
            return GetPropertyValue(attr, property, ref outVal);
        }

        private static bool IsStandardType(Type type) => type.IsPrimitive || type == typeof(DateTime) || type == typeof(DateTimeOffset) || type == typeof(Decimal) || type == typeof(Guid) || type == typeof(TimeSpan) || type == typeof(string) || type.IsEnum;

        private static bool HasMembers([NotNull] object obj)
        {
            if (obj is IXmlSerializable)
            {
                using (System.IO.MemoryStream mem = new System.IO.MemoryStream())
                {
                    using (XmlTextWriter tw = new XmlTextWriter(mem, Encoding.UTF8))
                    {
                        ((IXmlSerializable)obj).WriteXml(tw);
                        tw.Flush();
                        return mem.Length > 3;
                    }
                }
            }

            // Enumerate each public property
            PropertyInfo[] props = obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
            foreach (var pi in props)
            {
                if (!Attribute.IsDefined(pi, typeof(XmlIgnoreAttribute), false))
                {
                    object value = pi.GetValue(obj, null);
                    if (!Equals(value, GetDefaultValue(pi)))
                    {
                        if (!IsStandardType(pi.PropertyType))
                        {
                            if (HasMembers(value))
                                return true;
                        }
                        else
                            return true;
                    }
                }
            }
            return false;
        }

        public static string GetPropertyAttributeName([NotNull] PropertyInfo pi)
        {
            object oVal = null;
            string eName = pi.Name;
            if (GetAttributeValue(pi, typeof(XmlAttributeAttribute), "AttributeName", false, ref oVal))
                eName = oVal.ToString();
            return eName;
        }

        public static string GetPropertyElementName([NotNull] PropertyInfo pi)
        {
            object oVal = null;
            string eName = pi.Name;
            if (GetAttributeValue(pi, typeof(XmlElementAttribute), "ElementName", false, ref oVal))
                eName = oVal.ToString();
            else if (GetAttributeValue(pi.PropertyType, typeof(XmlRootAttribute), "ElementName", true, ref oVal))
                eName = oVal.ToString();
            return eName;
        }

        public delegate bool PropertyConversionHandler([NotNull] PropertyInfo pi, Object obj, ref Object value);

        public static bool WriteProperty([NotNull] XmlWriter writer, [NotNull] PropertyInfo pi, [NotNull] Object obj, PropertyConversionHandler handler = null)
        {
            if (Attribute.IsDefined(pi, typeof(XmlIgnoreAttribute), false) || Attribute.IsDefined(pi, typeof(XmlAttributeAttribute), false))
                return false;

            object value = pi.GetValue(obj, null);
            object defValue = GetDefaultValue(pi);
            if ((value == null && defValue == null) || (value != null && value.Equals(defValue)))
                return false;

            Type propType = pi.PropertyType;
            if (handler != null && handler(pi, obj, ref value))
                propType = value.GetType();

            bool isStdType = IsStandardType(propType);
            bool rw = pi.CanRead && pi.CanWrite;
            bool ro = pi.CanRead && !pi.CanWrite;
            string eName = GetPropertyElementName(pi);
            if (isStdType && rw)
            {
                string output = GetXmlValue(value, propType);
                if (output != null)
                    writer.WriteElementString(eName, output);
            }
            else if (!isStdType)
            {
                object outVal = null;
                if (propType.GetInterface("IXmlSerializable") == null && GetAttributeValue(pi, typeof(XmlArrayAttribute), "ElementName", true, ref outVal) && propType.GetInterface("IEnumerable") != null)
                {
                    if (string.IsNullOrEmpty(outVal.ToString())) outVal = eName;
                    writer.WriteStartElement(outVal.ToString());
                    var attributes = Attribute.GetCustomAttributes(pi, typeof(XmlArrayItemAttribute), true);
                    var dict = new Dictionary<Type, string>(attributes.Length);
                    foreach (XmlArrayItemAttribute a in attributes)
                        dict.Add(a.Type, a.ElementName);
                    foreach (object item in ((System.Collections.IEnumerable)value))
                    {
                        string aeName;
                        Type itemType = item.GetType();
                        if (dict.TryGetValue(itemType, out aeName))
                        {
                            if (IsStandardType(itemType))
                                writer.WriteElementString(aeName, GetXmlValue(item, itemType));
                            else
                                WriteObject(writer, item, null, false, aeName);
                        }
                    }
                    writer.WriteEndElement();
                }
                else
                    WriteObject(writer, value);
            }
            return false;
        }

        private static string GetXmlValue([NotNull] object value, Type propType)
        {
            string output = null;
            if (propType.IsEnum)
            {
                if (Attribute.IsDefined(propType, typeof(FlagsAttribute), false))
                    output = Convert.ChangeType(value, Enum.GetUnderlyingType(propType)).ToString();
                else
                    output = value.ToString();
            }
            else
            {
                switch (propType.FullName)
                {
                    case "System.Boolean":
                        output = XmlConvert.ToString((System.Boolean)value);
                        break;
                    case "System.Byte":
                        output = XmlConvert.ToString((System.Byte)value);
                        break;
                    case "System.Char":
                        output = XmlConvert.ToString((System.Char)value);
                        break;
                    case "System.DateTime":
                        output = XmlConvert.ToString((System.DateTime)value, XmlDateTimeSerializationMode.RoundtripKind);
                        break;
                    case "System.DateTimeOffset":
                        output = XmlConvert.ToString((System.DateTimeOffset)value);
                        break;
                    case "System.Decimal":
                        output = XmlConvert.ToString((System.Decimal)value);
                        break;
                    case "System.Double":
                        output = XmlConvert.ToString((System.Double)value);
                        break;
                    case "System.Single":
                        output = XmlConvert.ToString((System.Single)value);
                        break;
                    case "System.Guid":
                        output = XmlConvert.ToString((System.Guid)value);
                        break;
                    case "System.Int16":
                        output = XmlConvert.ToString((System.Int16)value);
                        break;
                    case "System.Int32":
                        output = XmlConvert.ToString((System.Int32)value);
                        break;
                    case "System.Int64":
                        output = XmlConvert.ToString((System.Int64)value);
                        break;
                    case "System.SByte":
                        output = XmlConvert.ToString((System.SByte)value);
                        break;
                    case "System.TimeSpan":
                        output = XmlConvert.ToString((System.TimeSpan)value);
                        break;
                    case "System.UInt16":
                        output = XmlConvert.ToString((System.UInt16)value);
                        break;
                    case "System.UInt32":
                        output = XmlConvert.ToString((System.UInt32)value);
                        break;
                    case "System.UInt64":
                        output = XmlConvert.ToString((System.UInt64)value);
                        break;
                    default:
                        output = value == null ? string.Empty : value.ToString();
                        break;
                }
            }

            return output;
        }

        public static void WriteObjectAttributes([NotNull] XmlWriter writer, [NotNull] object obj, PropertyConversionHandler handler = null)
        {
            // Enumerate each property
            foreach (var pi in obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
                if (Attribute.IsDefined(pi, typeof(XmlAttributeAttribute), false))
                    WriteObjectAttribute(writer, pi, obj, handler);
        }

        public static void WriteObjectAttribute([NotNull] XmlWriter writer, [NotNull] PropertyInfo pi, [NotNull] object obj, PropertyConversionHandler handler = null)
        {
            object value = pi.GetValue(obj, null);
            object defValue = GetDefaultValue(pi);
            if ((value == null && defValue == null) || (value != null && value.Equals(defValue)))
                return;

            Type propType = pi.PropertyType;
            if (handler != null && handler(pi, obj, ref value))
                propType = value.GetType();

            writer.WriteAttributeString(GetPropertyAttributeName(pi), GetXmlValue(value, propType));
        }

        public static void WriteObjectProperties([NotNull] XmlWriter writer, [NotNull] object obj, PropertyConversionHandler handler = null)
        {
            // Enumerate each public property
            foreach (var pi in obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
                WriteProperty(writer, pi, obj, handler);
        }

        public static void WriteObject([NotNull] XmlWriter writer, [NotNull] object obj, PropertyConversionHandler handler = null, bool includeNS = false, string elemName = null)
        {
            if (obj == null)
                return;

            // Get name of top level element
            string oName = elemName ?? GetElementName(obj);

            if (!HasMembers(obj))
                return;

            if (includeNS)
                writer.WriteStartElement(oName, GetTopLevelNamespace(obj));
            else
                writer.WriteStartElement(oName);

            if (obj is IXmlSerializable)
            {
                ((IXmlSerializable)obj).WriteXml(writer);
            }
            else
            {
                WriteObjectAttributes(writer, obj, handler);
                WriteObjectProperties(writer, obj, handler);
            }

            writer.WriteEndElement();
        }

        public static string GetElementName([NotNull] object obj)
        {
            object oVal = null;
            return GetAttributeValue(obj.GetType(), typeof(XmlRootAttribute), "ElementName", true, ref oVal) ? oVal.ToString() : obj.GetType().Name;
        }

        public static string GetTopLevelNamespace([NotNull] object obj)
        {
            object oVal = null;
            return GetAttributeValue(obj.GetType(), typeof(XmlRootAttribute), "Namespace", true, ref oVal) ? oVal.ToString() : null;
        }

        public static void ReadObjectProperties([NotNull] XmlReader reader, [NotNull] object obj, PropertyConversionHandler handler = null)
        {
            // Build property lookup table
            PropertyInfo[] props = obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
            Dictionary<string, PropertyInfo> attrHash = new Dictionary<string, PropertyInfo>(props.Length);
            Dictionary<string, PropertyInfo> propHash = new Dictionary<string, PropertyInfo>(props.Length);
            foreach (var pi in props)
            {
                if (!Attribute.IsDefined(pi, typeof(XmlIgnoreAttribute), false))
                {
                    if (Attribute.IsDefined(pi, typeof(XmlAttributeAttribute), false))
                        attrHash.Add(GetPropertyAttributeName(pi), pi);
                    else
                        propHash.Add(GetPropertyElementName(pi), pi);
                }
            }

            if (reader.HasAttributes)
            {
                for (int i = 0; i < reader.AttributeCount; i++)
                {
                    PropertyInfo pi;
                    reader.MoveToAttribute(i);
                    if (attrHash.TryGetValue(reader.LocalName, out pi))
                    {
                        if (IsStandardType(pi.PropertyType))
                        {
                            object value = null;
                            if (pi.PropertyType.IsEnum)
                                value = Enum.Parse(pi.PropertyType, reader.Value);
                            else
                                value = Convert.ChangeType(reader.Value, pi.PropertyType);

                            if (handler != null)
                                handler(pi, obj, ref value);

                            pi.SetValue(obj, value, null);
                        }
                    }
                }
            }

            while (reader.MoveToContent() == XmlNodeType.Element)
            {
                PropertyInfo pi;
                object outVal = null;
                if (propHash.TryGetValue(reader.LocalName, out pi))
                {
                    var tc = TypeDescriptor.GetConverter(pi.PropertyType);
                    if (IsStandardType(pi.PropertyType))
                    {
                        object value = null;
                        if (pi.PropertyType.IsEnum)
                            value = Enum.Parse(pi.PropertyType, reader.ReadElementContentAsString());
                        else if (pi.PropertyType == typeof(Guid))
                            value = new GuidConverter().ConvertFromString(reader.ReadElementContentAsString());
                        else
                            value = reader.ReadElementContentAs(pi.PropertyType, null);

                        if (handler != null)
                            handler(pi, obj, ref value);

                        pi.SetValue(obj, value, null);
                    }
                    else if (pi.PropertyType == typeof(Version))
                    {
                        Version v = new Version(reader.ReadElementContentAsString());
                        pi.SetValue(obj, v, null);
                    }
                    else if (pi.PropertyType.GetInterface("IEnumerable") != null && pi.PropertyType.GetInterface("IXmlSerializable") == null && GetAttributeValue(pi, typeof(XmlArrayAttribute), "ElementName", true, ref outVal))
                    {
                        string elem = string.IsNullOrEmpty(outVal?.ToString()) ? pi.Name : outVal.ToString();
                        reader.ReadStartElement(elem);
                        var attributes = Attribute.GetCustomAttributes(pi, typeof(XmlArrayItemAttribute), true);
                        var dict = new Dictionary<string, Type>(attributes.Length);
                        foreach (XmlArrayItemAttribute a in attributes)
                            dict.Add(a.ElementName, a.Type);
                        List<object> output = new List<object>();
                        while (reader.MoveToContent() == XmlNodeType.Element)
                        {
                            Type itemType;
                            if (dict.TryGetValue(reader.LocalName, out itemType))
                            {
                                object o;
                                if (IsStandardType(itemType))
                                    o = reader.ReadElementContentAs(itemType, null);
                                else
                                {
                                    o = Activator.CreateInstance(itemType);
                                    ReadObject(reader, o, handler);
                                }
                                if (o != null)
                                    output.Add(o);
                            }
                        }
                        reader.ReadEndElement();
                        if (output.Count > 0)
                        {
                            System.Collections.IEnumerable par = output;
                            Type et = typeof(object);
                            if (dict.Count == 1)
                            {
                                foreach (var v in dict.Values) { et = v; break; }
                            }
                            /*else
							{
								Type t1 = output[0].GetType();
								bool same = true;
								foreach (var item in output)
									if (item.GetType() != t1) { same = false; break; }
								if (same)
									et = t1;
							}
							if (et != typeof(object))
							{
								Array ao = Array.CreateInstance(et, output.Count);
								for (int i = 0; i < output.Count; i++)
									ao.SetValue(output[i], i);
								par = ao;
							}
							else
								par = output.ToArray();*/
                            bool done = false;
                            if (pi.PropertyType == par.GetType() || (pi.PropertyType.IsArray && (pi.PropertyType.GetElementType() == typeof(object) || pi.PropertyType.GetElementType() == et)))
                                try { pi.SetValue(obj, par, null); done = true; } catch { }
                            if (!done)
                            {
                                var mi = pi.PropertyType.GetMethod("AddRange", new Type[] { typeof(System.Collections.IEnumerable) });
                                if (mi != null)
                                    try { mi.Invoke(pi.GetValue(obj, null), new object[] { par }); done = true; } catch { }
                            }
                            if (!done)
                            {
                                var mi = pi.PropertyType.GetMethod("Add", new Type[] { typeof(object) });
                                if (mi != null)
                                    try { foreach (var i in par) mi.Invoke(pi.GetValue(obj, null), new object[] { i }); done = true; } catch { }
                            }
                            if (!done && et != typeof(Object))
                            {
                                var mi = pi.PropertyType.GetMethod("Add", new Type[] { et });
                                if (mi != null)
                                    try { foreach (var i in par) mi.Invoke(pi.GetValue(obj, null), new object[] { i }); done = true; } catch { }
                            }
                            // Throw error if not done
                        }
                    }
                    else
                    {
                        object inst = pi.GetValue(obj, null) ?? Activator.CreateInstance(pi.PropertyType);
                        if (inst == null)
                            throw new InvalidOperationException($"Can't get instance of {pi.PropertyType.Name}.");
                        ReadObject(reader, inst, handler);
                    }
                }
                else
                {
                    reader.Skip();
                    reader.MoveToContent();
                }
            }
        }

        public static void ReadObject([NotNull] XmlReader reader, [NotNull] object obj, PropertyConversionHandler handler = null)
        {
            if (obj == null)
                throw new ArgumentNullException(nameof(obj));

            reader.MoveToContent();

            if (obj is IXmlSerializable)
            {
                ((IXmlSerializable)obj).ReadXml(reader);
            }
            else
            {
                object oVal = null;
                string oName = GetAttributeValue(obj.GetType(), typeof(XmlRootAttribute), "ElementName", true, ref oVal) ? oVal.ToString() : obj.GetType().Name;
                if (reader.LocalName != oName)
                    throw new XmlException("XML element name does not match object.");

                if (!reader.IsEmptyElement)
                {
                    reader.ReadStartElement();
                    reader.MoveToContent();
                    ReadObjectProperties(reader, obj, handler);
                    reader.ReadEndElement();
                }
                else
                    reader.Skip();
            }
        }

        public static void ReadObjectFromXmlText([NotNull] string xml, [NotNull] object obj, PropertyConversionHandler handler = null)
        {
            using (System.IO.StringReader sr = new System.IO.StringReader(xml))
            {
                using (XmlReader reader = XmlReader.Create(sr))
                {
                    reader.MoveToContent();
                    ReadObject(reader, obj, handler);
                }
            }
        }

        public static string WriteObjectToXmlText([NotNull] object obj, PropertyConversionHandler handler = null)
        {
            StringBuilder sb = new StringBuilder();
            using (XmlWriter writer = XmlWriter.Create(sb, new XmlWriterSettings() { Indent = true }))
                WriteObject(writer, obj, handler, true);
            return sb.ToString();
        }
    }
}
